#include <ne.h>
#include <system/GlutApplication.h>

int Width;
int Height;

float SkyTextureSize = 512.0f;
float SkyBoxSize = 1024.0f;

std::string skyBoxImageMap[6] = {"../image/skybox/evening_RT.jpg",
                                 "../image/skybox/evening_LF.jpg",
                                 "../image/skybox/evening_UP.jpg",
                                 "../image/skybox/evening_DN.jpg",
                                 "../image/skybox/evening_BK.jpg",
                                 "../image/skybox/evening_FR.jpg"};

int lineMode = 0;
ne::Engine *pEngine = 0;
ne::Terrain *pTerrain = 0;
ne::TexturePtr pTexture;
ne::TexturePtr pSkyTexture;
ne::TexturePtr pTerrainTexture;
ne::GLSLProgramPtr pProgram;
ne::Light *light = 0;
ne::Timer timer;
ne::Camera *camera = 0;
ne::Viewport *viewport = 0;
ne::Vector2 mousePosition;
bool mouseLeftOn = false;
bool mouseRightOn = false;
ne::GlutApplication *pApplication = 0;

void mouseFunc(int button, int state, int x, int y);
void motionFunc(int x, int y);
void loop(void);
void redraw(void);
void drawFPS(void);
void resize(int width, int height);
void init();

int main(int argc, char *argv[]) /* I - Command-line arguments */
{
    pApplication = new ne::GlutApplication();//(argc, argv);
    glutReshapeFunc(resize);
    glutDisplayFunc(loop);
    glutMouseFunc(mouseFunc);
    glutMotionFunc(motionFunc);

    pEngine = pApplication->engine;
    if (!pEngine || !pEngine->isReady())
    {
        delete pApplication;
        return 0;
    }

    init();

    glutMainLoop();

    delete pApplication;

    return 0;
}

void init()
{
    pTerrainTexture = pEngine->getTextureManager()->createTexture("Terrain Texture", "Terrain Texture",
        ne::TT_3D, ne::TF_RGBA, ne::PT_UNSIGNED_BYTE, 512, 512, 3);
    ne::ImagePtr pImage = pEngine->getImageManager()->create("t0", "../image/t0.png");
    pImage->load();
    pTerrainTexture->loadData(pImage->getData(),
                              pImage->getFormat(),
                              pImage->getPixelType(),
                              0, 0, 0,
                              pImage->getWidth(), pImage->getHeight(), 1);
    pEngine->getImageManager()->removeByPtr(pImage);

    pImage = pEngine->getImageManager()->create("t1", "../image/t1.png");
    pImage->load();
    pTerrainTexture->loadData(pImage->getData(),
                              pImage->getFormat(),
                              pImage->getPixelType(),
                              0, 0, 1,
                              pImage->getWidth(), pImage->getHeight(), 1);
    pEngine->getImageManager()->removeByPtr(pImage);

    pImage = pEngine->getImageManager()->create("t2", "../image/t2.png");
    pImage->load();
    pTerrainTexture->loadData(pImage->getData(),
                              pImage->getFormat(),
                              pImage->getPixelType(),
                              0, 0, 2,
                              pImage->getWidth(), pImage->getHeight(), 1);
    pEngine->getImageManager()->removeByPtr(pImage);

    pTerrainTexture->setNumMipmap(9);
    pTerrainTexture->setWrap(ne::TWP_REPEAT, ne::TWP_REPEAT, ne::TWP_REPEAT);
    pTerrainTexture->setFilter(ne::TFF_LINEAR_MIPMAP_LINEAR, ne::TFF_LINEAR);
    pTerrainTexture->setLOD(0, 9, -0.5f);

    pImage = pEngine->getImageManager()->create("height", "../image/terrain.png");
    pImage->load();
    pTerrain = new ne::Terrain("Terrain");
    pTerrain->generateTerrainData(pImage.ptr());
    pEngine->getImageManager()->removeByPtr(pImage);

    pTexture = pEngine->getTextureManager()->createTextureFromFile("t", "../image/terrain.png", ne::TF_RGBA);
    pTexture->load();
    pTexture->createRenderTexture();

    std::cout << "Finished generate Terrain Data\n";

    pSkyTexture = pEngine->getTextureManager()->createTexture("SkyBox", "SkyBox", ne::TT_CUBE_MAP, ne::TF_RGB, ne::PT_UNSIGNED_BYTE, SkyTextureSize, SkyTextureSize);
    for (size_t i=0; i<6; ++i)
    {
        pImage = pEngine->getImageManager()->create("SkyBoxImage", skyBoxImageMap[i]);
        pImage->load();
        pSkyTexture->loadImageForCubeMap(ne::TextureCubeMapFace(ne::TCMF_POSITIVE_X + i), *pImage);
        pEngine->getImageManager()->removeByPtr(pImage);
    }
    pSkyTexture->createRenderTexture();

    camera = new ne::Camera();
    viewport = new ne::Viewport(camera);
    camera->setFarClipDistance(4096.0f);
    camera->setPosition(ne::Vector3(512.0f, 0.0f, 1024.0f));

    glClearColor(0, 0, 0, 0);
    glClearDepth(1);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glDepthFunc(GL_LEQUAL);
    glEnable(GL_DEPTH_TEST);

    glCullFace(GL_BACK);
    glEnable(GL_CULL_FACE);

    //glEnable(GL_COLOR_MATERIAL);
    //glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);
    glDisable(GL_LIGHTING);

    glShadeModel(GL_SMOOTH);
    glEnable(GL_POINT_SMOOTH);
    glHint(GL_POINT_SMOOTH_HINT, GL_NICEST);
    glEnable(GL_LINE_SMOOTH);
    glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
    glEnable(GL_POLYGON_SMOOTH);
    glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST);

    // Set light
    light = new ne::Light();
    light->setType(ne::LT_POINT);
    light->setAmbient(ne::Color::GRAY);
    light->setAttenuationFactor(ne::LAT_LINEAR, 0.002f);
    light->setPosition(ne::Vector3(0.0f, 500.0f, 0.0f));

    ne::GLSLManager *pManager = pEngine->getGLSLManager();
    pProgram = pManager->create("Terrain", "../shader/terrain.glpg", true);
    ne::GLSLShaderPtr pVShader;
    pVShader = pProgram->createShaderFromFile("TerrainVertexShader", "../shader/terrain.glvs", ne::SHADER_TYPE_VERTEX);
    ne::GLSLShaderPtr pFShader;
    pFShader = pProgram->createShaderFromFile("TerrainFragmentShader", "../shader/terrain.glfs", ne::SHADER_TYPE_FRAGMENT);
    pProgram->attachShader(pVShader);
    pProgram->attachShader(pFShader);
    pProgram->link();
}

void mouseFunc(int button, int state, int x, int y)
{
    if (button == GLUT_LEFT_BUTTON && state == GLUT_DOWN)
    {
        mousePosition.x = x;
        mousePosition.y = y;
        mouseLeftOn = true;
    }
    else if (button == GLUT_LEFT_BUTTON && state == GLUT_UP)
    {
        mouseLeftOn = false;
    }
    if (button == GLUT_RIGHT_BUTTON && state == GLUT_DOWN)
    {
        mousePosition.x = x;
        mousePosition.y = y;
        mouseRightOn = true;
    }
    else if (button == GLUT_RIGHT_BUTTON && state == GLUT_UP)
    {
        mouseRightOn = false;
    }
}

void motionFunc(int x, int y)
{
    if (mouseLeftOn)
    {
    }
    if (mouseRightOn)
    {
        float dx = x - mousePosition.x;
        float dy = y - mousePosition.y;
        camera->yaw(-0.01f * dx * ne::Math::PI / 36, ne::TS_WORLD);
        camera->pitch(-0.01f * dy * ne::Math::PI / 36);
        mousePosition.x = x;
        mousePosition.y = y;
    }
}

void loop(void)
{
    ne::Keyboard *pKeyboard = pEngine->getKeyboard();
    ne::real speed = 0.1f;
    if (pKeyboard->isKeyDown(ne::KK_F12))
    {
        speed = 1.0f;
    }
    if (pKeyboard->isKeyDown(ne::KK_W))
    {
        camera->translate(speed * ne::Vector3::NEGATIVE_UNIT_Z, ne::TS_LOCAL);
    }
    if (pKeyboard->isKeyDown(ne::KK_S))
    {
        camera->translate(speed * ne::Vector3::UNIT_Z, ne::TS_LOCAL);
    }
    if (pKeyboard->isKeyDown(ne::KK_A))
    {
        camera->translate(speed * ne::Vector3::NEGATIVE_UNIT_X, ne::TS_LOCAL);
    }
    if (pKeyboard->isKeyDown(ne::KK_D))
    {
        camera->translate(speed * ne::Vector3::UNIT_X, ne::TS_LOCAL);
    }
    if (pKeyboard->isKeyDown(ne::KK_Z))
    {
        camera->translate(speed * ne::Vector3::NEGATIVE_UNIT_Y, ne::TS_PARENT);
    }
    if (pKeyboard->isKeyDown(ne::KK_Q))
    {
        camera->translate(speed * ne::Vector3::UNIT_Y, ne::TS_PARENT);
    }
    if (pKeyboard->isKeyDown(ne::KK_P))
    {
        lineMode = true;
    }
    else
    {
        lineMode = false;
    }
    if (pKeyboard->isKeyDown(ne::KK_ESC))
    {
        exit(1);
    }

    pTerrain->update(viewport);
    ne::Vector3 pos = camera->getPosition();
    float h = pTerrain->getHeightAt(pos);
    if (pos.y < h + 2.0f)
    {
        pos.y = h + 2.0f;
        camera->setPosition(pos);
    }
    redraw();
}

void drawSky(void)
{
    glDisable(GL_DEPTH_TEST);
    glDepthMask(GL_FALSE);
    glDisable(GL_LIGHTING);

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(pSkyTexture->getType(), pSkyTexture->getSource());
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
    glEnable(pSkyTexture->getType());

    glBegin(GL_QUADS);
        glTexCoord3f(-1.0f, 1.0f, 1.0f);
        glVertex3f(-SkyBoxSize, SkyBoxSize, SkyBoxSize);
        glTexCoord3f(-1.0f, -1.0f, 1.0f);
        glVertex3f(-SkyBoxSize, -SkyBoxSize, SkyBoxSize);
        glTexCoord3f(-1.0f, -1.0f, -1.0f);
        glVertex3f(-SkyBoxSize, -SkyBoxSize, -SkyBoxSize);
        glTexCoord3f(-1.0f, 1.0f, -1.0f);
        glVertex3f(-SkyBoxSize, SkyBoxSize, -SkyBoxSize);

        glTexCoord3f(1.0f, 1.0f, 1.0f);
        glVertex3f(SkyBoxSize, SkyBoxSize, SkyBoxSize);
        glTexCoord3f(1.0f, 1.0f, -1.0f);
        glVertex3f(SkyBoxSize, SkyBoxSize, -SkyBoxSize);
        glTexCoord3f(1.0f, -1.0f, -1.0f);
        glVertex3f(SkyBoxSize, -SkyBoxSize, -SkyBoxSize);
        glTexCoord3f(1.0f, -1.0f, 1.0f);
        glVertex3f(SkyBoxSize, -SkyBoxSize, SkyBoxSize);

        glTexCoord3f(-1.0f, -1.0f, -1.0f);
        glVertex3f(-SkyBoxSize, -SkyBoxSize, -SkyBoxSize);
        glTexCoord3f(-1.0f, -1.0f, 1.0f);
        glVertex3f(-SkyBoxSize, -SkyBoxSize, SkyBoxSize);
        glTexCoord3f(1.0f, -1.0f, 1.0f);
        glVertex3f(SkyBoxSize, -SkyBoxSize, SkyBoxSize);
        glTexCoord3f(1.0f, -1.0f, -1.0f);
        glVertex3f(SkyBoxSize, -SkyBoxSize, -SkyBoxSize);

        glTexCoord3f(-1.0f, 1.0f, -1.0f);
        glVertex3f(-SkyBoxSize, SkyBoxSize, -SkyBoxSize);
        glTexCoord3f(1.0f, 1.0f, -1.0f);
        glVertex3f(SkyBoxSize, SkyBoxSize, -SkyBoxSize);
        glTexCoord3f(1.0f, 1.0f, 1.0f);
        glVertex3f(SkyBoxSize, SkyBoxSize, SkyBoxSize);
        glTexCoord3f(-1.0f, 1.0f, 1.0f);
        glVertex3f(-SkyBoxSize, SkyBoxSize, SkyBoxSize);

        glTexCoord3f(1.0f, 1.0f, -1.0f);
        glVertex3f(SkyBoxSize, SkyBoxSize, -SkyBoxSize);
        glTexCoord3f(-1.0f, 1.0f, -1.0f);
        glVertex3f(-SkyBoxSize, SkyBoxSize, -SkyBoxSize);
        glTexCoord3f(-1.0f, -1.0f, -1.0f);
        glVertex3f(-SkyBoxSize, -SkyBoxSize, -SkyBoxSize);
        glTexCoord3f(1.0f, -1.0f, -1.0f);
        glVertex3f(SkyBoxSize, -SkyBoxSize, -SkyBoxSize);

        glTexCoord3f(1.0f, 1.0f, 1.0f);
        glVertex3f(SkyBoxSize, SkyBoxSize, SkyBoxSize);
        glTexCoord3f(1.0f, -1.0f, 1.0f);
        glVertex3f(SkyBoxSize, -SkyBoxSize, SkyBoxSize);
        glTexCoord3f(-1.0f, -1.0f, 1.0f);
        glVertex3f(-SkyBoxSize, -SkyBoxSize, SkyBoxSize);
        glTexCoord3f(-1.0f, 1.0f, 1.0f);
        glVertex3f(-SkyBoxSize, SkyBoxSize, SkyBoxSize);
    glEnd();
    glDisable(pSkyTexture->getType());

    glEnable(GL_DEPTH_TEST);
    glDepthMask(GL_TRUE);
    glEnable(GL_LIGHTING);
}

void redraw(void)
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glMatrixMode(GL_MODELVIEW);
    const ne::Matrix4 &viewMatrix = camera->getViewMatrix();
    glLoadMatrixf(viewMatrix.ptr());

    glPushMatrix();
        ne::Vector3 pos = camera->getPosition();
        glTranslatef(pos.x, pos.y, pos.z);
        drawSky();
    glPopMatrix();

    glEnable(GL_COLOR_SUM);

    if (lineMode)
    {
        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    }
    else
    {
        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    }

    glPushMatrix();
        if (pTexture.notNull())
        {
            glActiveTexture(GL_TEXTURE0);
            pTexture->createRenderTexture();
            glBindTexture(pTexture->getType(), pTexture->getSource());
            //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
            glEnable(pTexture->getType());
        }

        if (pTerrainTexture.notNull())
        {
            glActiveTexture(GL_TEXTURE1);
            pTerrainTexture->createRenderTexture();
            glBindTexture(pTexture->getType(), pTexture->getSource());
            //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
            glEnable(pTexture->getType());
        }

        pProgram->useProgram();
        pProgram->setUniform("cameraPos", camera->getPosition());
        pProgram->setUniform("largeMap", 0);
        pProgram->setUniform("textureMap", 1);
        pProgram->validate();

        const std::vector<ne::Vector4> &vertexList = pTerrain->getVertexList();
        const std::vector<ne::Vector3> &normalList = pTerrain->getNormalList();
        const std::vector<ne::Color> &colorList = pTerrain->getColorList();
        const std::vector<uint32_t> &indexList = pTerrain->getIndexList();

        glVertexPointer(4, GL_FLOAT, 0, vertexList[0].ptr());
        glEnableClientState(GL_VERTEX_ARRAY);

        glNormalPointer(GL_FLOAT, 0, normalList[0].ptr());
        glEnableClientState(GL_NORMAL_ARRAY);

        glColorPointer(4, GL_FLOAT, 0, colorList[0].ptr());
        glEnableClientState(GL_COLOR_ARRAY);

        glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
        glDrawElements(GL_TRIANGLES, indexList.size(), GL_UNSIGNED_INT, &indexList[0]);

        glDisableClientState(GL_VERTEX_ARRAY);
        glDisableClientState(GL_NORMAL_ARRAY);
        glDisableClientState(GL_COLOR_ARRAY);

        pProgram->useProgram(false);
        glDisable(pTexture->getType());
        glActiveTexture(GL_TEXTURE0);

    glPopMatrix();

    glDisable(GL_COLOR_SUM);

    glDisable(GL_LIGHTING);

    glPolygonMode(GL_FRONT, GL_FILL);

    drawFPS();

    glutSwapBuffers();
    glutPostRedisplay();
}

void resize(int width, int height)
{
    /* Save the new width and height */
    Width  = width;
    Height = height;

    /* Reset the viewport->.. */
    glViewport(0, 0, width, height);
    viewport->setPosition(0, 0);
    viewport->resize(width, height);

    glMatrixMode(GL_PROJECTION);
    const ne::Matrix4 &projectionMatrix = camera->getProjectionMatrix();
    glLoadMatrixf(projectionMatrix.ptr());
    glMatrixMode(GL_MODELVIEW);
}

void drawFPS()
{
    glClear(GL_DEPTH_BUFFER_BIT);

    //Set matrices for ortho
    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();
    glOrtho(0.0f, viewport->getWidth(),
        viewport->getHeight(), 0.0f,
        1.0f, -1.0f);

    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glLoadIdentity();

    //Print text
    ne::FontPtr font = pEngine->getFont();
    ne::Vector3 pos(Width - 300.0f, 10.0f, 0.0f);
    float w = font->getFontWidth();
    float h = font->getFontHeight();

    char fpsString[32];
    double interval = timer.getIntervalMilliseconds();
    sprintf(fpsString, "FPS : %.2lf", 1000.0f / interval);
    font->renderText2D(fpsString, pos, 32.0f*w, h, ne::Color(1.0f, 0.0f, 0.0f, 1.0f));

    char posString[32];
    ne::Vector3 cameraPos = camera->getPosition();
    sprintf(posString, "Pos : %.2f, %.2f, %.2f", cameraPos.x, cameraPos.y, cameraPos.z);
    pos.y += 12.0f;
    font->renderText2D(posString, pos, 32.0f*w, h, ne::Color(0.0f, 0.6f, 1.0f, 1.0f));

    char verString[32];
    const std::vector<uint32_t> &indexList = pTerrain->getIndexList();
    sprintf(verString, "Vertex : %d", (int)indexList.size());
    pos.y += 12.0f;
    font->renderText2D(verString, pos, 32.0f*w, h, ne::Color(0.0f, 1.0f, 0.0f, 1.0f));

    //reset matrices
    glMatrixMode(GL_PROJECTION);
    glPopMatrix();
    glMatrixMode(GL_MODELVIEW);
    glPopMatrix();
}

